package io.github.eric.urpc.core.rpc;

import io.github.eric.urpc.core.config.AbstractContext;
import io.github.eric.urpc.core.exception.UrpcMsgRegException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.Arrays;
import java.util.List;

public class RpcMappingResolver {
    private static final Logger LOGGER = LoggerFactory.getLogger(RpcMappingResolver.class);

    public void resolve(AbstractContext abstractContext) {
        List<RpcController> rpcControllers = abstractContext.getRpcControllers();
        MessageRegistrar messageRegistrar = abstractContext.getMessageRegistrar();
        for (RpcController rpcController : rpcControllers) {
            Class<?> serviceType = rpcController.getClass();
            Method[] methods = serviceType.getDeclaredMethods();
            for (Method method : methods) {
                RpcMethod rpcMethod = method.getDeclaredAnnotation(RpcMethod.class);
                if (rpcMethod != null) {
                    int modifiers = method.getModifiers();
                    if (!Modifier.isPublic(modifiers)) {
                        throw new UrpcMsgRegException(String.format("[urpc] rpc method '%s' should only be declared with 'public'",
                                method.getName()));
                    }
                    if (Modifier.isStatic(modifiers)) {
                        throw new UrpcMsgRegException(String.format("[urpc] rpc method '%s' should not be declared with 'static'",
                                method.getName()));
                    }
                    Class<?>[] parameterTypes = method.getParameterTypes();
                    if (parameterTypes.length == 0 || parameterTypes.length > 2) {
                        throw new UrpcMsgRegException(String.format("[urpc] rpc method '%s' params size should not greater than 2 or equal to 0",
                                method.getName()));
                    }
                    Class<?> msgType = null;
                    int rpcContextIndex = -1;
                    if (parameterTypes.length == 1) {
                        msgType = parameterTypes[0];
                        validateMsgAndReturnTypeCompatibility(msgType, method.getReturnType(), method);
                    }
                    if (parameterTypes.length == 2) {
                        if (Arrays.stream(parameterTypes).filter(t -> t == RpcContext.class).count() != 1) {
                            throw new UrpcMsgRegException(String.format("[urpc] rpc method '%s' param size is 2, then need a RpcContext type as param",
                                    method.getName(), Message.class.getName()));
                        }
                        rpcContextIndex = parameterTypes[0] == RpcContext.class ? 0 : 1;
                        msgType = rpcContextIndex == 0 ? parameterTypes[1] : parameterTypes[0];
                        validateMsgAndReturnTypeCompatibility(msgType, method.getReturnType(), method);
                    }
                    messageRegistrar.registerCallback(msgType, rpcController, method, rpcContextIndex);

                    abstractContext.getLogger().info("[urpc] register rpc method: " + msgType.getSimpleName() + " ----> " + serviceType.getSimpleName() + "." + method.getName());
                }
            }
        }
    }

    /**
     * msgType -> returnType
     *
     * request -> response : good
     * request -> void : good
     * request -> message : bad
     * request -> request : bad
     *
     * message -> message : good
     * message -> void : good
     * message -> response : bad
     * message -> request : bad
     */
    private void validateMsgAndReturnTypeCompatibility(Class<?> msgType, Class<?> returnType, Method method) {
        if (!Message.class.isAssignableFrom(msgType)) {
            throw new UrpcMsgRegException(String.format("[urpc] rpc method '%s' msg type should only extends '%s'",
                    method.getName(), Message.class.getName()));
        }
        if (Request.class.isAssignableFrom(msgType)) {
            if (returnType != void.class && !Response.class.isAssignableFrom(returnType)) {
                throw new UrpcMsgRegException(String.format("[urpc] rpc method '%s' return type could be void or extends '%s' if msg type extends '%s'",
                        method.getName(), Response.class.getName(), Request.class.getName()));
            }
        } else {
            if (returnType != void.class && (!Message.class.isAssignableFrom(returnType)
                    || Response.class.isAssignableFrom(returnType) || Request.class.isAssignableFrom(returnType))) {
                throw new UrpcMsgRegException(String.format("[urpc] rpc method '%s' return type could be void or extends '%s' if msg type extends '%s'",
                        method.getName(), Message.class.getName(), Message.class.getName()));
            }
        }
    }
}
